// SPDX-FileCopyrightText: Adam Evyčędo
//
// SPDX-License-Identifier: GPL-3.0-or-later

package xyz.apiote.bimba.czwek.data.sources

import android.content.Context
import android.util.Base64
import android.util.Log
import androidx.core.content.edit
import com.charleskorn.kaml.Yaml
import com.charleskorn.kaml.decodeFromStream
import com.github.jershell.kbson.KBson
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flow
import kotlinx.serialization.Serializable
import xyz.apiote.bimba.czwek.R
import xyz.apiote.bimba.czwek.api.Error
import xyz.apiote.bimba.czwek.api.LineV1
import xyz.apiote.bimba.czwek.api.LineV2
import xyz.apiote.bimba.czwek.api.LineV3
import xyz.apiote.bimba.czwek.api.Result
import xyz.apiote.bimba.czwek.api.StopV1
import xyz.apiote.bimba.czwek.api.StopV2
import xyz.apiote.bimba.czwek.api.StopV3
import xyz.apiote.bimba.czwek.api.Traffic
import xyz.apiote.bimba.czwek.api.TrafficServer
import xyz.apiote.bimba.czwek.api.UnknownResourceException
import xyz.apiote.bimba.czwek.api.VehicleV1
import xyz.apiote.bimba.czwek.api.VehicleV2
import xyz.apiote.bimba.czwek.api.VehicleV3
import xyz.apiote.bimba.czwek.api.hostWithScheme
import xyz.apiote.bimba.czwek.api.rawRequest
import xyz.apiote.bimba.czwek.api.request
import xyz.apiote.bimba.czwek.api.responses.ErrorResponse
import xyz.apiote.bimba.czwek.api.responses.FeedsResponse
import xyz.apiote.bimba.czwek.api.responses.FeedsResponseDev
import xyz.apiote.bimba.czwek.api.responses.FeedsResponseV1
import xyz.apiote.bimba.czwek.api.responses.FeedsResponseV2
import xyz.apiote.bimba.czwek.api.responses.LocatablesResponse
import xyz.apiote.bimba.czwek.api.responses.LocatablesResponseDev
import xyz.apiote.bimba.czwek.api.responses.LocatablesResponseV1
import xyz.apiote.bimba.czwek.api.responses.LocatablesResponseV2
import xyz.apiote.bimba.czwek.api.responses.LocatablesResponseV3
import xyz.apiote.bimba.czwek.api.responses.QueryablesResponse
import xyz.apiote.bimba.czwek.api.responses.QueryablesResponseDev
import xyz.apiote.bimba.czwek.api.responses.QueryablesResponseV1
import xyz.apiote.bimba.czwek.api.responses.QueryablesResponseV2
import xyz.apiote.bimba.czwek.api.responses.QueryablesResponseV3
import xyz.apiote.bimba.czwek.api.responses.QueryablesResponseV4
import xyz.apiote.bimba.czwek.data.FeedInfoFlowItem
import xyz.apiote.bimba.czwek.data.ResultFlowItem
import xyz.apiote.bimba.czwek.data.exceptions.BimbaException
import xyz.apiote.bimba.czwek.network.mapError
import xyz.apiote.bimba.czwek.repo.FeedInfo
import xyz.apiote.bimba.czwek.repo.Line
import xyz.apiote.bimba.czwek.repo.Locatable
import xyz.apiote.bimba.czwek.repo.Position
import xyz.apiote.bimba.czwek.repo.Queryable
import xyz.apiote.bimba.czwek.repo.Stop
import xyz.apiote.bimba.czwek.repo.Vehicle
import java.io.InputStream
import java.net.MalformedURLException
import java.net.URL

@Serializable
data class Server(
	var host: String,
	var traffic: Traffic?
) {
	companion object {
		const val DEFAULT = "bimba.app"

		const val SERVER_KEY = "server"

		fun get(context: Context): Server {
			val preferences = context.getSharedPreferences("shp", Context.MODE_PRIVATE)
			val savedServer = preferences.getString(SERVER_KEY, null)
			return savedServer?.let {
				KBson().load(serializer(), Base64.decode(it, Base64.DEFAULT))
			} ?: Server(DEFAULT, null)
		}
	}

	fun save(context: Context) {
		val savedServer = Base64.encodeToString(KBson().dump(serializer(), this), Base64.DEFAULT)
		val preferences = context.getSharedPreferences("shp", Context.MODE_PRIVATE)
		preferences.edit {
			putString(SERVER_KEY, savedServer)
		}
	}

	fun getSelectedServer(): TrafficServer {
		return traffic!!.servers[traffic!!.selectedServer]
	}

	suspend fun getTraffic(context: Context, force: Boolean = false) {
		if (traffic != null && !force) {
			save(context)
			return
		}
		val result = try {
			rawRequest(
				URL("${hostWithScheme(host)}/.well-known/traffic.yml"), context, emptyArray()
			)
		} catch (_: MalformedURLException) {
			Result(null, Error(0, R.string.error_url, R.drawable.error_url))
		}
		if (result.error != null) {
			Log.e("Server", "while getting traffic: ${result.error}")
		} else {
			val traffic = Yaml.default.decodeFromStream<Traffic>(result.stream!!)
			if (this.traffic == null) {
				this.traffic = traffic
			} else {
				val servers = traffic.servers.mapIndexed { i, server ->
					if (this.getSelectedServer().url == server.url) {
						traffic.selectedServer = i
					}
					server
				}
				this.traffic =
					Traffic(traffic.authEndpoint, traffic.accountEndpoint, servers, traffic.selectedServer)
			}

			save(context)
		}
	}

	fun queryQueryables(
		query: String,
		context: Context,
		ignoreNotFound: Boolean = false,
		limit: Int? = null
	): Flow<ResultFlowItem> =
		flow {
			val params = mutableMapOf("q" to query)
			if (limit != null) {
				params["limit"] = limit.toString()
			}
			val result = try {
				request(
					this@Server,
					"queryables",
					null,
					params,
					context,
					arrayOf(1u, 2u, 3u, 4u),
					null
				)
			} catch (e: BimbaException) {
				emit(e)
				return@flow
			}

			if (result.error != null) {
				if (result.error.statusCode == 404 && ignoreNotFound) {
					return@flow
				}

				val message = if (result.stream != null) {
					ErrorResponse.unmarshal(result.stream).message
				} else {
					null
				}
				emit(mapError(result.error.statusCode, message))
				return@flow
			}

			parseQueryables(result.stream!!).forEach {
				emit(it as ResultFlowItem)
			}
		}

	fun locateQueryables(
		position: Position,
		context: Context,
	): Flow<ResultFlowItem> = flow {
		val result = try {
			request(
				this@Server,
				"queryables",
				null,
				mapOf("near" to position.toString()),
				context,
				arrayOf(1u, 2u, 3u, 4u),
				null
			)
		} catch (e: BimbaException) {
			emit(e)
			return@flow
		}

		if (result.error != null) {
			val message = if (result.stream != null) {
				ErrorResponse.unmarshal(result.stream).message
			} else {
				null
			}
			emit(mapError(result.error.statusCode, message))
			return@flow
		}

		parseQueryables(result.stream!!).forEach {
			emit(it as ResultFlowItem)
		}
	}

	private fun parseQueryables(stream: InputStream): List<Queryable> {
		return when (val response =
			QueryablesResponse.unmarshal(stream)) {
			is QueryablesResponseDev -> response.queryables.map {
				when (it) {
					is StopV3 -> Stop(it)
					is LineV3 -> Line(it)
					else -> throw UnknownResourceException("queryablesV4", it::class)
				}
			}

			is QueryablesResponseV1 -> response.queryables.map {
				when (it) {
					is StopV1 -> Stop(it)
					else -> throw UnknownResourceException("queryablesV1", it::class)
				}
			}

			is QueryablesResponseV2 -> response.queryables.map {
				when (it) {
					is StopV2 -> Stop(it)
					is LineV1 -> Line(it)
					else -> throw UnknownResourceException("queryablesV2", it::class)
				}
			}

			is QueryablesResponseV3 -> response.queryables.map {
				when (it) {
					is StopV2 -> Stop(it)
					is LineV2 -> Line(it)
					else -> throw UnknownResourceException("queryablesV3", it::class)
				}
			}

			is QueryablesResponseV4 -> response.queryables.map {
				when (it) {
					is StopV2 -> Stop(it)
					is LineV3 -> Line(it)
					else -> throw UnknownResourceException("queryablesV4", it::class)
				}
			}

			else -> null
		} ?: emptyList()
	}

	fun getFeedInfos(context: Context): Flow<FeedInfoFlowItem> = flow {
		var result: Result
		try {
			result = rawRequest(
				URL(getSelectedServer().url), context, arrayOf(1u, 2u)
			)
		} catch (e: BimbaException) {
			emit(e)
			return@flow
		}

		Log.i("getFeedInfos Online", "$result")

		if (result.error != null) {
			val message = if (result.stream != null) {
				ErrorResponse.unmarshal(result.stream).message
			} else {
				null
			}
			emit(mapError(result.error.statusCode, message))
			return@flow
		}

		when (val response = FeedsResponse.unmarshal(result.stream!!)) {
			is FeedsResponseDev -> response.feeds.map { emit(FeedInfo(it)) }
			is FeedsResponseV2 -> response.feeds.map { emit(FeedInfo(it)) }
			is FeedsResponseV1 -> response.feeds.map { emit(FeedInfo(it)) }

			else -> null
		}
	}

	fun getLocatablesIn(
		context: Context,
		bl: Position,
		tr: Position
	): Flow<ResultFlowItem> = flow {
		val result = try {
			request(
				this@Server,
				"locatables",
				null,
				mapOf("lb" to bl.toString(), "rt" to tr.toString()),
				context,
				arrayOf(1u, 2u, 3u),
				null
			)
		} catch (e: BimbaException) {
			emit(e)
			return@flow
		}

		if (result.error != null) {
			val message = if (result.stream != null) {
				ErrorResponse.unmarshal(result.stream).message
			} else {
				null
			}
			emit(mapError(result.error.statusCode, message))
			return@flow
		}

		parseLocatables(result.stream!!).forEach { emit(it as ResultFlowItem) }
	}

	private fun parseLocatables(stream: InputStream): List<Locatable> {
		return when (val response =
			LocatablesResponse.unmarshal(stream)) {
			is LocatablesResponseDev -> response.locatables.map {
				when (it) {
					is StopV3 -> Stop(it)
					is VehicleV3 -> Vehicle(it)
					else -> throw UnknownResourceException("locatables", it::class)
				}
			}

			is LocatablesResponseV3 -> response.locatables.map {
				when (it) {
					is StopV2 -> Stop(it)
					is VehicleV3 -> Vehicle(it)
					else -> throw UnknownResourceException("locatables", it::class)
				}
			}

			is LocatablesResponseV2 -> response.locatables.map {
				when (it) {
					is StopV2 -> Stop(it)
					is VehicleV2 -> Vehicle(it)
					else -> throw UnknownResourceException("locatables", it::class)
				}
			}

			is LocatablesResponseV1 -> response.locatables.map {
				when (it) {
					is StopV1 -> Stop(it)
					is VehicleV1 -> Vehicle(it)
					else -> throw UnknownResourceException("locatables", it::class)
				}
			}

			else -> null
		} ?: emptyList()
	}
}